/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package agen.mutacion;

import agen.AGenApp;
import agen.optimalidad.Aptitud;
import agen.util.exception.MetodoInexistente;
import agen.util.exception.PorcentajesInconsistentes;
import java.util.Arrays;
import java.util.Random;

/**
 *
 * @author juan
 */
public class Mutacion {

    public static final int ADAPTATVA_POR_CONVERGENCIA = 0;
    public static final int ADAPTATVA_POR_TEMPERATURA_DECRESIENTE = 1;
    public static final int ADAPTATVA_POR_TEMPERATURA_CRESIENTE = 2;
    public static final int SIMPLE = 3;
    private static int metodoElegido = ADAPTATVA_POR_CONVERGENCIA;
    private static double temperatura = 25,  deltaTemperatura = 0.05,  
            probMutacion = 0.05,  distanciaMax = 0,  minProbMutacion = 0.001,
            maxProbMutacion = 0.7;

    public static double getProbMutacion() {
        return probMutacion;
    }

    public static void setProbMutacion(float probMutacion) {
        Mutacion.probMutacion = probMutacion;
    }

    /**
     * Como la cantidad de individuos a insertar en poblacionModificada es
     * preestablecida, optamos por alterar sólo la probabilidad de mutacion de
     * cada gen, de modo que la sumatoria de las probabilidades de cada gen
     * arrojen la prob del individuo. De este modo, si la Pm(individuo) es baja
     * habría una buena chance de que el individuo, escojido al azar pase sin
     * mutar a población modificada.
     *
     * @param offset
     * @param poblacionActual
     * @param poblacionModificada
     * @param aptitudes
     * @param metodoElegido
     * @param eltosXIndividuo
     * @param aptitudesOrdenadas
     * @return
     * @throws agen.util.exception.PorcentajesInconsistentes
     * @throws agen.util.exception.MetodoInexistente
     */
    public static int mutacion(int offset, int porcentaje, byte[] poblacionActual,
            byte[] poblacionModificada, double[] aptitudes, int metodoElegido,
            int eltosXIndividuo)
            throws MetodoInexistente {

        if (metodoElegido != 0) {
            Mutacion.metodoElegido = metodoElegido;
        }
        if (!(metodoElegido == ADAPTATVA_POR_CONVERGENCIA ||
                metodoElegido == ADAPTATVA_POR_TEMPERATURA_CRESIENTE ||
                metodoElegido == ADAPTATVA_POR_TEMPERATURA_DECRESIENTE ||
                metodoElegido == SIMPLE)) {
            throw new MetodoInexistente("Mutacion.mutacion: el metodo en"
                    + " metodoElegido no está definido");
        }
        return aplicarMetodos(offset, porcentaje, poblacionActual, poblacionModificada, 
                aptitudes, eltosXIndividuo);
    }

    public static double getTemperatura() {
        return temperatura;
    }

    public static void setTemperatura(float temperatura) {
        Mutacion.temperatura = temperatura;
    }

    private static int aplicarMetodos(int offset, int porcentaje,
            byte[] poblacionActual, byte[] poblacionModificada,
            double[] aptitudes, int eltosXIndividuo) {
        double probMutacionElto = 0;
        int len = (int) Math.round((porcentaje / 100.0) * aptitudes.length);
        /* si el redondeo hace que se pase el indice ajustamos en el ultimo
         * intrvalo*/
        if (offset + len > aptitudes.length) {
            len = aptitudes.length - offset;
        }
        switch (metodoElegido) {
            case ADAPTATVA_POR_CONVERGENCIA:
                probMutacionElto = porConvergencia(poblacionActual, eltosXIndividuo);
                break;
            case ADAPTATVA_POR_TEMPERATURA_CRESIENTE:
                probMutacionElto = porTemperatura(true);
                break;
            case ADAPTATVA_POR_TEMPERATURA_DECRESIENTE:
                probMutacionElto = porTemperatura(false);
                break;
            case SIMPLE:
                probMutacionElto = probMutacion;// / (double) eltosXIndividuo;
                probMutacionElto = probMutacionElto < minProbMutacion ?
                    minProbMutacion : probMutacionElto;
                break;
        }
        mutar(probMutacionElto, poblacionActual, poblacionModificada, offset,
                len, eltosXIndividuo);
        return offset + len;
    }

    private static void mutar(double probMutacionElto, byte[] poblacionActual,
            byte[] poblacionModificada,
            int offset, int len, int eltosXIndividuo) {
        byte eltoMutado;
        int indiceIndividuo;
        Random r1 = new Random();
        for (int i = offset * eltosXIndividuo; i < (offset + len) * eltosXIndividuo
                ; i += eltosXIndividuo) {
            indiceIndividuo = r1.nextInt(poblacionActual.length);
            indiceIndividuo -= indiceIndividuo % eltosXIndividuo;
            for (int j = eltosXIndividuo - 1; j >= 0; j--) {
                if (r1.nextDouble() < probMutacionElto) {
                    eltoMutado = (byte) r1.nextInt(16);
//                    System.err.println("mute al elto: " + j
//                            + " del individuo: "+indiceIndividuo/eltosXIndividuo);
                    while (eltoMutado == poblacionActual[indiceIndividuo + j]) {
                        eltoMutado = (byte) r1.nextInt(16);
                    }
                } else {
                    eltoMutado = poblacionActual[indiceIndividuo + j];
                }
                poblacionModificada[i] = eltoMutado;
            }
        }
    }

    /**
     * Calcula el centro de masa en un espacion n-Dimencional con n =
     * elementos por individuo. La distancia promedio al centro de masa de la
     * población, relativa al maximo histórico obtenido.
     * determina el grado de convergencia.
     *
     * @param poblacion
     * @param eltosXIndividuo
     * @return   un double entre 0 y 1. Lo esperable es que devuelva un valor
     *           entre 0 y 0,6. Cero implica que todos los individuos estan
     *           sobre el mismo punto, de generacion en generacion se espera una
     *          aglomeracion creciente, y 0,6 porque si los individuos estan
     *          aleatoriamente distribuidos lo normal sería que el promedio de
     *          distancias ronde la mitad del maximo.
     */
    public static double calcularConvergenciaIndividuos(byte[] poblacion,
            int eltosXIndividuo) {
        float[] centroDeMasa = new float[eltosXIndividuo];
        double distanciaAlCentro = 0, acumDistanciaAlCentro = 0, dAux;
        Arrays.fill(centroDeMasa, 0);
        centroMasaIndividuos(centroDeMasa, poblacion, eltosXIndividuo);
        for (int i = poblacion.length - 1; i >= 0; i--) {
            distanciaAlCentro += Math.pow(poblacion[i]
                    - centroDeMasa[i % eltosXIndividuo], 2);
            if (i % eltosXIndividuo == 0) {
                dAux = Math.sqrt(distanciaAlCentro);
                if (dAux > distanciaMax) {
                    distanciaMax = dAux;
                }
                acumDistanciaAlCentro += dAux;
                distanciaAlCentro = 0;
            }
        }
        acumDistanciaAlCentro /= (poblacion.length / (float) eltosXIndividuo);
        System.err.println("Convergencia: " + acumDistanciaAlCentro / distanciaMax);
        return acumDistanciaAlCentro / distanciaMax;
    }

    /**
     * Las aptitudes estan calculadas para la generacion previa al cruce, por lo
     * que existe un desfasaje respecto de la realidad con la convergencia de
     * las aptitudes de una generacion.
     * Calcula la distancia promedio al promedio de las aptitudes.
     */
    public static double calcularConvergenciaAptitudes(double[] aptitudes, int generacion) {
        /* Cómo las aptitudes se calculan una vez por generación, es factible
         * calcular el promedio en el momento en que se obtienen.
         */
        double promedioAptitudes = Aptitud.getPromedioAptitudes(generacion);
        double distancia = 0;
        for (int i = aptitudes.length - 1; i >= 0; i--) {
            distancia += Math.abs(aptitudes[i] - promedioAptitudes);
        }
        distancia /= (double)aptitudes.length;
        return distancia;
    }

    /**
     * promedio de cada coordenada o dimensión del individuo.
     */
    private static void centroMasaIndividuos(float[] centroDeMasa, byte[] poblacion,
            int eltosXIndividuo) {
        for (int i = poblacion.length - 1; i >= 0; i--) {
            centroDeMasa[i % eltosXIndividuo] += poblacion[i];
        }
        for (int i = 0; i < centroDeMasa.length; i++) {
            centroDeMasa[i] /= (poblacion.length / (float) eltosXIndividuo);
        }
    }

    private static double porConvergencia(byte[] poblacionActual,
            int eltosXIndividuo) {
        double convergencia = calcularConvergenciaIndividuos(poblacionActual,
                eltosXIndividuo);
        /*improbable o imposible llegar a maxProbMutación. Esto sucederia si
         * convergencia = 0 (todos sobre el mismo punto).
         */
        double probMutacionElto = (maxProbMutacion * (1 - convergencia));// / (double) eltosXIndividuo;
        probMutacionElto = probMutacionElto < minProbMutacion ?
            minProbMutacion : probMutacionElto;
        return probMutacionElto;
    }

    /**
     * La temperatura puede variar entre 0 y 100. Cuando es 100 la probabilidad de
     * mutación es maxProbMutacion.
     */
    private static double porTemperatura(boolean creciente) {
        if (creciente) {
            temperatura = Math.min(100, temperatura * (1 + deltaTemperatura));
        } else {
            temperatura = Math.max(0, temperatura * (1 - deltaTemperatura));
        }
        double probMutacionElto = (maxProbMutacion * (temperatura / 100.0));
        // / (double) eltosXIndividuo;
        probMutacionElto = probMutacionElto < minProbMutacion ?
            minProbMutacion : probMutacionElto;
        return probMutacionElto;
    }
}
